Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[dotnet] [bidi] Use JsonSerializerContext to be AOT friendly #15162

Merged
merged 7 commits into from
Jan 28, 2025

Conversation

nvborisenko
Copy link
Member

@nvborisenko nvborisenko commented Jan 26, 2025

User description

Description

Introduced BiDiSerializerContext for json serialization.

Motivation and Context

Contributes to #14480

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • I have read the contributing document.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

PR Type

Enhancement, Tests


Description

  • Introduced BiDiSerializerContext for JSON serialization.

  • Replaced JsonSerializerOptions with JsonSerializerContext across BiDi components.

  • Added a new BiDiSerializerContext class with extensive type mappings.

  • Updated transport interfaces and implementations to use JsonSerializerContext.


Changes walkthrough 📝

Relevant files
Enhancement
Broker.cs
Refactor Broker to use BiDiSerializerContext                         

dotnet/src/webdriver/BiDi/Communication/Broker.cs

  • Replaced JsonSerializerOptions with BiDiSerializerContext.
  • Updated JSON serialization and deserialization calls.
  • Adjusted constructor to initialize BiDiSerializerContext.
  • +9/-6     
    BiDiSerializerContext.cs
    Introduce BiDiSerializerContext for JSON serialization     

    dotnet/src/webdriver/BiDi/Communication/Json/BiDiSerializerContext.cs

  • Added new BiDiSerializerContext class.
  • Defined extensive type mappings for JSON serialization.
  • Utilized JsonSerializable attributes for type definitions.
  • +118/-0 
    ITransport.cs
    Update ITransport interface for JsonSerializerContext       

    dotnet/src/webdriver/BiDi/Communication/Transport/ITransport.cs

  • Updated ITransport interface to use JsonSerializerContext.
  • Replaced JsonSerializerOptions in method signatures.
  • +3/-3     
    WebSocketTransport.cs
    Refactor WebSocketTransport for JsonSerializerContext       

    dotnet/src/webdriver/BiDi/Communication/Transport/WebSocketTransport.cs

  • Refactored WebSocketTransport to use JsonSerializerContext.
  • Updated serialization and deserialization logic.
  • +5/-4     

    Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.
  • Copy link
    Contributor

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
    🧪 No relevant tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Error Handling

    The JsonSerializer error handling should be validated to ensure proper exception handling when deserialization fails, especially for the ReceiveMessagesAsync method which runs in a loop.

    var message = await _transport.ReceiveAsJsonAsync<Message>(_jsonSerializerContext, cancellationToken);
    Type Coverage

    Verify that all possible BiDi message types are covered in the JsonSerializable attributes list to avoid runtime serialization errors.

    [JsonSerializable(typeof(MessageSuccess))]
    [JsonSerializable(typeof(MessageError))]
    [JsonSerializable(typeof(MessageEvent))]
    
    [JsonSerializable(typeof(Modules.Script.EvaluateResult.Success))]
    [JsonSerializable(typeof(Modules.Script.EvaluateResult.Exception))]
    
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Number), TypeInfoPropertyName = "Script_RemoteValue_Number")]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Boolean), TypeInfoPropertyName = "Script_RemoteValue_Boolean")]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.String), TypeInfoPropertyName = "Script_RemoteValue_String")]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Null), TypeInfoPropertyName = "Script_RemoteValue_Null")]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Undefined), TypeInfoPropertyName = "Script_RemoteValue_Undefined")]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Symbol))]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Array), TypeInfoPropertyName = "Script_RemoteValue_Array")]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Object), TypeInfoPropertyName = "Script_RemoteValue_Object")]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Function))]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.RegExp), TypeInfoPropertyName = "Script_RemoteValue_RegExp")]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.RegExp.RegExpValue), TypeInfoPropertyName = "Script_RemoteValue_RegExp_RegExpValue")]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Date), TypeInfoPropertyName = "Script_RemoteValue_Date")]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Map), TypeInfoPropertyName = "Script_RemoteValue_Map")]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Set), TypeInfoPropertyName = "Script_RemoteValue_Set")]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.WeakMap))]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.WeakSet))]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Generator))]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Error))]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Proxy))]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Promise))]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.TypedArray))]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.ArrayBuffer))]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.NodeList))]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.HtmlCollection))]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.Node))]
    [JsonSerializable(typeof(Modules.Script.RemoteValue.WindowProxy))]
    
    [JsonSerializable(typeof(Modules.Script.LocalValue.String), TypeInfoPropertyName = "Script_LocalValue_String")]
    
    [JsonSerializable(typeof(Modules.Script.Target.Realm), TypeInfoPropertyName = "Script_Target_Realm")]
    [JsonSerializable(typeof(Modules.Script.Target.Context), TypeInfoPropertyName = "Script_Target_Context")]
    
    [JsonSerializable(typeof(Modules.Script.RealmInfo.Window))]
    [JsonSerializable(typeof(Modules.Script.RealmInfo.DedicatedWorker))]
    [JsonSerializable(typeof(Modules.Script.RealmInfo.SharedWorker))]
    [JsonSerializable(typeof(Modules.Script.RealmInfo.ServiceWorker))]
    [JsonSerializable(typeof(Modules.Script.RealmInfo.Worker))]
    [JsonSerializable(typeof(Modules.Script.RealmInfo.PaintWorklet))]
    [JsonSerializable(typeof(Modules.Script.RealmInfo.AudioWorklet))]
    [JsonSerializable(typeof(Modules.Script.RealmInfo.Worklet))]
    
    [JsonSerializable(typeof(Modules.Log.Entry.Console))]
    [JsonSerializable(typeof(Modules.Log.Entry.Javascript))]
    //
    
    [JsonSerializable(typeof(Command))]
    [JsonSerializable(typeof(Message))]
    
    [JsonSerializable(typeof(Modules.Session.StatusResult))]
    [JsonSerializable(typeof(Modules.Session.NewResult))]
    
    [JsonSerializable(typeof(Modules.Browser.CloseCommand), TypeInfoPropertyName = "Browser_CloseCommand")]
    [JsonSerializable(typeof(Modules.Browser.GetUserContextsResult))]
    [JsonSerializable(typeof(IReadOnlyList<Modules.Browser.UserContextInfo>))]
    
    [JsonSerializable(typeof(Modules.BrowsingContext.CloseCommand), TypeInfoPropertyName = "BrowsingContext_CloseCommand")]
    [JsonSerializable(typeof(Modules.BrowsingContext.CreateResult))]
    [JsonSerializable(typeof(Modules.BrowsingContext.BrowsingContextInfo))]
    [JsonSerializable(typeof(Modules.BrowsingContext.NavigateResult))]
    [JsonSerializable(typeof(Modules.BrowsingContext.NavigationInfo))]
    [JsonSerializable(typeof(Modules.BrowsingContext.TraverseHistoryResult))]
    [JsonSerializable(typeof(Modules.BrowsingContext.LocateNodesResult))]
    [JsonSerializable(typeof(Modules.BrowsingContext.CaptureScreenshotResult))]
    [JsonSerializable(typeof(Modules.BrowsingContext.GetTreeResult))]
    [JsonSerializable(typeof(Modules.BrowsingContext.PrintResult))]
    [JsonSerializable(typeof(Modules.BrowsingContext.UserPromptOpenedEventArgs))]
    [JsonSerializable(typeof(Modules.BrowsingContext.UserPromptClosedEventArgs))]
    [JsonSerializable(typeof(Modules.BrowsingContext.Origin), TypeInfoPropertyName = "BrowsingContext_Origin")]
    
    [JsonSerializable(typeof(Modules.Network.BytesValue.String), TypeInfoPropertyName = "Network_BytesValue_String")]
    [JsonSerializable(typeof(Modules.Network.UrlPattern.String), TypeInfoPropertyName = "Network_UrlPattern_String")]
    [JsonSerializable(typeof(Modules.Network.ContinueWithAuthParameters.Default), TypeInfoPropertyName = "Network_ContinueWithAuthParameters_Default")]
    [JsonSerializable(typeof(Modules.Network.AddInterceptResult))]
    [JsonSerializable(typeof(Modules.Network.BeforeRequestSentEventArgs))]
    [JsonSerializable(typeof(Modules.Network.ResponseStartedEventArgs))]
    [JsonSerializable(typeof(Modules.Network.ResponseCompletedEventArgs))]
    [JsonSerializable(typeof(Modules.Network.FetchErrorEventArgs))]
    [JsonSerializable(typeof(Modules.Network.AuthRequiredEventArgs))]
    
    [JsonSerializable(typeof(Modules.Script.Channel), TypeInfoPropertyName = "Script_Channel")]
    [JsonSerializable(typeof(Modules.Script.AddPreloadScriptResult))]
    [JsonSerializable(typeof(Modules.Script.EvaluateResult))]
    [JsonSerializable(typeof(Modules.Script.GetRealmsResult))]
    [JsonSerializable(typeof(Modules.Script.MessageEventArgs))]
    [JsonSerializable(typeof(Modules.Script.RealmDestroyedEventArgs))]
    [JsonSerializable(typeof(IReadOnlyList<Modules.Script.RealmInfo>))]
    
    [JsonSerializable(typeof(Modules.Log.Entry))]
    
    [JsonSerializable(typeof(Modules.Storage.GetCookiesResult))]
    [JsonSerializable(typeof(Modules.Storage.DeleteCookiesResult))]
    [JsonSerializable(typeof(Modules.Storage.SetCookieResult))]
    
    [JsonSerializable(typeof(Modules.Input.PerformActionsCommand))]
    [JsonSerializable(typeof(Modules.Input.Pointer.Down), TypeInfoPropertyName = "Input_Pointer_Down")]
    [JsonSerializable(typeof(Modules.Input.Pointer.Up), TypeInfoPropertyName = "Input_Pointer_Up")]
    [JsonSerializable(typeof(Modules.Input.Pointer.Move), TypeInfoPropertyName = "Input_Pointer_Move")]
    [JsonSerializable(typeof(Modules.Input.Key.Down), TypeInfoPropertyName = "Input_Key_Down")]
    [JsonSerializable(typeof(Modules.Input.Key.Up), TypeInfoPropertyName = "Input_Key_Up")]
    [JsonSerializable(typeof(IEnumerable<Modules.Input.IPointerSourceAction>))]
    [JsonSerializable(typeof(IEnumerable<Modules.Input.IKeySourceAction>))]
    [JsonSerializable(typeof(IEnumerable<Modules.Input.INoneSourceAction>))]
    [JsonSerializable(typeof(IEnumerable<Modules.Input.IWheelSourceAction>))]
    
    internal partial class BiDiSerializerContext: JsonSerializerContext;

    Copy link
    Contributor

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Score
    Possible issue
    Add null check for safety

    Add null check for message before the switch statement to avoid potential
    NullReferenceException if message deserialization fails

    dotnet/src/webdriver/BiDi/Communication/Broker.cs [120-124]

     var message = await _transport.ReceiveAsJsonAsync<Message>(_jsonSerializerContext, cancellationToken);
    +if (message == null)
    +{
    +    continue;
    +}
     
     switch (message)
     {
         case MessageSuccess messageSuccess:
    • Apply this suggestion
    Suggestion importance[1-10]: 7

    Why: Adding a null check before the switch statement is a good defensive programming practice that prevents potential NullReferenceException if message deserialization fails, improving the code's robustness and error handling.

    7
    Learned
    best practice
    Add proper null validation before casting deserialized JSON results to prevent potential null reference exceptions

    The ExecuteCommandAsync method performs an unsafe cast using the ! operator without
    proper null checking. Add null validation before casting the deserialized result to
    prevent potential NullReferenceException.

    dotnet/src/webdriver/BiDi/Communication/Broker.cs [183]

    -return (TResult)((JsonElement)result).Deserialize(typeof(TResult), _jsonSerializerContext)!;
    +var deserialized = ((JsonElement)result).Deserialize(typeof(TResult), _jsonSerializerContext);
    +if (deserialized == null)
    +    throw new InvalidOperationException($"Failed to deserialize response to type {typeof(TResult).Name}");
    +return (TResult)deserialized;
    • Apply this suggestion
    6

    Copy link
    Contributor

    @RenderMichael RenderMichael left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Looks nice! I ran into the System.Runtime.CompilerServices.Unsafe issue earlier, happy to see a fix for it here.

    @nvborisenko nvborisenko changed the title [dotnet] [bidi] Use JsonSerializerContext [dotnet] [bidi] Use JsonSerializerContext to be AOT friendly Jan 28, 2025
    @nvborisenko nvborisenko merged commit 8d0f6af into SeleniumHQ:trunk Jan 28, 2025
    10 checks passed
    @nvborisenko nvborisenko deleted the dotnet-aot-bidi branch January 28, 2025 19:50
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    2 participants